package com.jerome.fs.http;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

/**
 * HTTP message parser for processing HTTP requests and responses.
 * This class provides methods to decode HTTP headers and messages,
 * as well as parsing them into Request and Response objects.
 *
 * @author Jerome
 * @date 2023/8/5
 */
public class HttpMessageParser {
    /**
     * Decode request headers based on the standard HTTP protocol.
     *
     * @param reader  BufferedReader for reading request headers.
     * @param request Request object for storing request information.
     * @throws IOException When an I/O error occurs while reading request headers.
     */
    private static void decodeRequestHeader(BufferedReader reader, Request request) throws IOException {
        // Create a Map object to store request header information
        Map<String, String> headers = new HashMap<String, String>(16);
        // Read request headers, each line is a key-value pair, terminated by an empty line
        String line = reader.readLine();
        String[] kv;
        while (!"".equals(line)) {
            // Split each line of request header using a colon as a separator and store in the Map
            kv = line.split(HttpConstant.HEADER_SEPARATOR);
            assert kv.length == 2;
            headers.put(kv[0].trim(), kv[1].trim());
            line = reader.readLine();
        }
        // Set the parsed request header information into the Request object
        request.setHeaders(headers);
    }

    /**
     * Decode the request message body based on the standard HTTP protocol.
     *
     * @param reader  InputStreamReader for reading the request body.
     * @param request Request object representing the HTTP request.
     * @throws IOException When an I/O error occurs.
     */
    private static void decodeRequestMessage(BufferedReader reader, Request request) throws IOException {
        // Get the Content-Length from the request headers, default to 0 if not present
        int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length", "0"));

        // If Content-Length is 0, there is no request message body, so return
        // For example, GET and OPTIONS requests typically do not have a request message body
        if (contentLen == 0) {
            return;
        }

        // Create a character array to store the request message body based on Content-Length
        char[] message = new char[contentLen];

        // Read the request message body using BufferedReader
        reader.read(message);

        // Convert the character array to a string and set it as the message in the Request object
        request.setMessage(new String(message));
    }

    /**
     * Parse HTTP request data from an InputStream and create a Request object.
     *
     * @param reqStream InputStream containing HTTP request data.
     * @return A Request object representing the HTTP request.
     * @throws IOException When an I/O error occurs.
     */
    public static Request parse2request(InputStream reqStream) throws IOException {
        // Use BufferedReader and InputStreamReader to read data from the input stream
        BufferedReader httpReader = new BufferedReader(new InputStreamReader(reqStream, "UTF-8"));

        // Create a new Request object
        Request httpRequest = new Request();

        // Decode the request line and set it into the Request object
        decodeRequestLine(httpReader, httpRequest);

        // Decode the request header and set it into the Request object
        decodeRequestHeader(httpReader, httpRequest);

        // Decode the request message body and set it into the Request object
        decodeRequestMessage(httpReader, httpRequest);

        // Return the parsed Request object
        return httpRequest;
    }

    /**
     * Parse HTTP response data from a BufferedReader and create a Response object.
     *
     * @param in BufferedReader containing HTTP response data.
     * @return A Response object representing the HTTP response.
     * @throws IOException When an I/O error occurs.
     */
    public static Response parseResponse(BufferedReader in) throws IOException {
        Response response = new Response();

        // Parse the first line to get the HTTP version and status code
        String firstLine = in.readLine();
        String[] parts = firstLine.split(HttpConstant.SPACE);
        response.setVersion(parts[0]);
        response.setCode(Integer.parseInt(parts[1]));
        response.setStatus(parts[2]);

        // Parse response headers
        String line;
        while ((line = in.readLine()) != null && !line.isEmpty()) {
            String[] headerParts = line.split(HttpConstant.HEADER_SEPARATOR, 2);
            if (headerParts.length == 2) {
                response.getHeaders().put(headerParts[0], headerParts[1]);
            }
        }

        // Parse the response body
        StringBuilder responseBody = new StringBuilder();
        while ((line = in.readLine()) != null) {
            responseBody.append(line);
        }
        response.setMessage(responseBody.toString());

        return response;
    }

    /**
     * Build an HTTP response string based on a Response object.
     *
     * @param response The Response object to build the response from.
     * @return A string representing the HTTP response.
     */
    public static String buildHttpResponse(Response response) {
        // Build the response string
        StringBuilder builder = new StringBuilder();
        buildResponseLine(response, builder);
        buildResponseHeaders(response, builder);
        buildResponseMessage(response, builder);
        return builder.toString();
    }

    /**
     * Build an error Response.
     *
     * @param e Exception.
     * @return A Response object representing the error.
     */
    public static Response buildErrorResponse(Exception e) {
        Response response = new Response();
        response.setCode(HttpStatus.HTTP_INTERNAL_ERROR);
        response.setStatus(HttpStatus.msg(HttpStatus.HTTP_INTERNAL_ERROR));
        response.setVersion(HttpConstant.HTTP_VERSION);
        response.setMessage(e.getMessage() == null ? "" : e.getMessage());
        return response;
    }

    /**
     * Build the response line, including version, status code, and status message.
     *
     * @param response      The Response object used to build the response line.
     * @param stringBuilder The StringBuilder for building the response string.
     */
    private static void buildResponseLine(Response response, StringBuilder stringBuilder) {
        stringBuilder.append(response.getVersion()).append(HttpConstant.SPACE).append(response.getCode()).append(HttpConstant.SPACE)
                .append(response.getStatus()).append(HttpConstant.LINE_SEPARATOR);
    }

    /**
     * Build response headers.
     *
     * @param response      The Response object used to build the response headers.
     * @param stringBuilder The StringBuilder for building the response string.
     */
    private static void buildResponseHeaders(Response response, StringBuilder stringBuilder) {
        for (Map.Entry<String, String> entry : response.getHeaders().entrySet()) {
            stringBuilder.append(entry.getKey()).append(HttpConstant.HEADER_SEPARATOR).append(entry.getValue()).append(HttpConstant.LINE_SEPARATOR);
        }
        stringBuilder.append(HttpConstant.LINE_SEPARATOR);
    }

    /**
     * Decode the request line based on the standard HTTP protocol.
     *
     * @param reader  BufferedReader for reading the request line.
     * @param request Request object for storing the request line.
     * @throws IOException When an I/O error occurs while reading the request line.
     */
    private static void decodeRequestLine(BufferedReader reader, Request request) throws IOException {
        String[] strs = reader.readLine().split(HttpConstant.SPACE);
        assert strs.length == 3;
        request.setMethod(strs[0]);
        request.setUri(strs[1]);
        request.setVersion(strs[2]);
    }

    /**
     * Build the response message body.
     *
     * @param response      The Response object used to build the response message body.
     * @param stringBuilder The StringBuilder for building the response string.
     */
    private static void buildResponseMessage(Response response, StringBuilder stringBuilder) {
        stringBuilder.append(response.getMessage());
    }
}
